package com.xxxx.comment.utils;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.xxxx.comment.constants.RedisConstants;
import jakarta.annotation.Resource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;


/**
 * 基于StringRedisTemplate封装一个缓存工具类，有下列功能：<p>
 * 方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间<p>
 * 方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题<p>
 * 方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题<p>
 * 方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题<p>
 *
 * @author lzf
 * @date 2023/8/31 10:10:41
 */
@Slf4j
@Setter
@Component
public class RedisCacheClient
{
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 设置键的同时设置过期时间
     * @param key 键
     * @param data 数据（任意类型）
     * @param timeLong 过期的时长
     * @param timeUnit 过期时长的时间单位
     */
    public <T> void set(String key, T data, Long timeLong, TimeUnit timeUnit)
    {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), timeLong, timeUnit);
    }
    
    public <T, ID> T getWithTTL(String keyPre, ID id, Class<T> type, Function<ID, T> doFallback,
                                Long timeLong, TimeUnit timeUnit)
    {
        String key = keyPre + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 命中缓存，反序列化后返回
        if (StringUtils.hasText(json))
            return JSONUtil.toBean(json, type);
        // 判断命中的是否是“空值缓存”
        if (ObjectUtil.isNull(json))
            return null;
        // 否则，根据id进入数据库查询数据
        T bean = doFallback.apply(id);
        // 数据库中不存在该条数据，但为了防止缓存穿透或缓存击穿，写一个“空值缓存”到redis
        if (bean == null)
        {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, timeUnit);
            return null;
        }
        // 数据库中存在该条数据，缓存到redis中
        set(key, JSONUtil.toJsonStr(bean), timeLong, timeUnit);
        
        return bean;
    }
    
    /**
     * 设置键的同时设置逻辑过期时间
     * @param key 键
     * @param data 数据（任意类型）
     * @param timeLong 过期的时长
     * @param timeUnit 过期时长的时间单位
     */
    public <T> void setWithLogicExpire(String key, T data, Long timeLong, TimeUnit timeUnit)
    {
        RedisData redisData =
                new RedisData((LocalDateTime) data, LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeLong)));
        
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    
    /**
     * 获取 设置了逻辑过期时间 的键
     * @param keyPre 键的前缀
     * @param lockKeyPre 对应锁的键的前缀
     * @param id 查询参数
     * @param type 从redis获取的json的反序列类型
     * @param doFallback 函数,jdk1.8新特性
     * @param timeLong 逻辑过期的时长
     * @param timeUnit 时长的时间单位
     * @return 获取到的json反序列化后的对象
     */
    public <T, ID> T getWithLogicExpire(String keyPre, String lockKeyPre, ID id, Class<T> type,
                                        Function<ID, T> doFallback, Long timeLong,
                                        TimeUnit timeUnit)
    {
        String key = keyPre + id;
        // 1、查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2、判断是否存在(不为null,不为""(空字符串)，不为"  "(空白字符串))
        if (!StringUtils.hasText(json))
            return null;
        // 3、命中缓存，反序列化对象,JSON反序列化就是这么垃圾，嵌套的对象不能序列化
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        T bean = JSONUtil.toBean((String)redisData.getData(), type);
        // 4、判断是否逻辑过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now()))
            return bean;
        // 5、过期，则开启一个新线程进行缓存重建
        String lockKey = lockKeyPre + id;
        if (tryLock(lockKey))
            new Thread(() -> {
                try
                {
                    // 查询数据库，获取数据
                    T data = doFallback.apply(id);
                    // 重建缓存
                    setWithLogicExpire(key, data, timeLong, timeUnit);
                }
                catch (Exception e)
                {
                    log.error(e.getMessage());
                    throw new RuntimeException();
                }
                finally
                {
                    unLock(lockKey);
                }
            });
        // 开启线程重建缓存后，返回过期的商铺信息
        return bean;
    }
    
    public <T, ID> T getWithMutex(String keyPre, String lockKeyPre, ID id, Class<T> type,
                                  Function<ID, T> doFallback, Long timeLong, TimeUnit timeUnit)
    {
        String key = keyPre + id;
        // 查询店铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.hasText(json))
            return JSONUtil.toBean(json, type);
        // 判断命中的是否为之前为了防止缓存击穿而设置的“空值缓存”
        if (!ObjectUtil.isNull(json))
            return null; // 不为null，说明命中了“缓存击穿的预防缓存”，直接返回null即可
        String lockKey = lockKeyPre + id;
        T bean;
        try
        {
            // 否则，是之前没有查询过的数据，去数据库中查询
            Boolean isLock = tryLock(lockKey);
            if (!isLock)
            {
                // 获取锁失败，休眠50ms并重试
                Thread.sleep(50);
                return getWithMutex(keyPre, lockKeyPre, id, type, doFallback, timeLong, timeUnit);
            }
            bean = doFallback.apply(id);
            // 数据库中没有这条数据
            if (bean == null)
            {
                stringRedisTemplate.opsForValue().set(key, "",
                        RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 如果查询到了这条数据的话，构建缓存
           this.set(key, JSONUtil.toJsonStr(bean), timeLong, timeUnit);
        }
        catch (Exception e)
        {
            log.error(e.getMessage());
            throw new RuntimeException();
        }
        finally
        {
            unLock(lockKey);
        }
        
        return bean;
    }
    
    private Boolean tryLock(String key)
    {
        return stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
    }
    
    private void unLock(String key)
    {
        stringRedisTemplate.delete(key);
    }
}
